<!-- 虽然页面简单，但是逻辑复杂，建议新建 vue 文件实现 Monaco Editor 相关逻辑 -->
<template>
  <div class="editor-instance"></div>
</template>

<script setup lang="ts">
import { editor, IRange, languages } from "monaco-editor";
import { nextTick, onMounted, ref, toRaw } from "vue";
import { fixMonacoEditorEnvError } from "../../../utils/monaco";
import {
  IEditorEvent,
  IEditorStateMap,
  IKeyMap,
  voidFun,
} from "../../../interface";
import WebContainerAPI from "../../../class/WebContainerAPI";
import { ElMessage } from "element-plus";

const emit = defineEmits(["windowResize"]);

// 定义当前编辑器实例对象
const editorInstance = ref(<editor.IStandaloneCodeEditor | null>null);

// 定义当前编辑器的数据视图映射对象 - 数据映射肯定是最优解，避免每次都创建新的数据模型，节省开销
const viewStateMap = <IEditorStateMap>new Map();

// 定义当前打开的文件的 ID
const currentFileID = ref("");

/**
 * 初始化编辑器
 */
function initEditor() {
  if (editorInstance.value) return;
  // 1. 修复worker 异常问题
  fixMonacoEditorEnvError();
  // 2. 创建 editor 实例
  const dom = document.querySelector(".editor-instance") as HTMLElement;
  // 4. 创建编辑器实例
  editorInstance.value = editor.create(dom);
  Reflect.set(window, "editor", getEditorInstance());
  // 5. 监听事件
  editorInstance.value.onKeyDown(keydownHandle);
  // 6. 监听resize
  window.addEventListener("resize", windowResize);
  // 7. 决定用什么主题是由 html 的 data-theme 属性决定
  const name = <string>document.documentElement.getAttribute("data-theme");
  // 做主题映射
  const themeMap: IKeyMap<string, string> = { dark: "vs-dark", light: "vs" };
  // vs， vs-dark，hc-black
  editor.setTheme(themeMap[name]);
}

/**
 * 销毁编辑器
 */
function destroy() {
  getEditorInstance()?.dispose();
  editorInstance.value = null;
  window.removeEventListener("resize", windowResize);
}

function windowResize() {
  // 计算合适的宽高
  emit("windowResize");
}

/**
 * 重新渲染编辑器大小
 */
async function resize(payload?: { width: number; height: number }) {
  await nextTick();
  // 延迟渲染
  getEditorInstance()?.layout(payload, true);
}

/**
 *  Monaco Editor 多tab页切换实现思路:
 *   1. editor.saveViewState() 获取当前编辑器的状态
 *   2. editor.restoreViewState 恢复编辑器状态
 *   3. editor.focus() 获取焦点
 *   4. createModel、getModel、setModel
 *
 * 设置编辑器的值 + 设置语言模型（外部调用的关键方法）
 * @param content 编辑器的值
 * @param suffix 文件后缀 - 通过后缀识别语言模型
 * @param id 文件 id
 */
function setEditorContent(payload: {
  id: string;
  content?: string;
  suffix?: string;
}) {
  const instance = getEditorInstance();
  if (!instance) return;

  const { id, suffix, content = "" } = payload;

  currentFileID.value = id;

  // 这里不一定就直接 setEditorContent ，判断是否已经存在模型，存在直接取，不存在要保存
  const haveViewState = viewStateMap.get(id);

  if (haveViewState && haveViewState.model) {
    setModel(toRaw(haveViewState.model));
    restoreViewState(toRaw(haveViewState.state)); // 恢复文件的编辑状态
    return;
  }

  if (!suffix) return;

  // 修改语言模型（修改文件语言类型 js、html css 等）
  const languageModelId = getLanguageModel(suffix);
  // 创建语言模型
  const model = createModel(content, languageModelId);
  // 设置语言模型
  setModel(model);

  // 保存当前文件的状态，以便后续再次打开时，直接加载状态即可
  const state = saveViewState() as editor.ICodeEditorViewState;
  viewStateMap.set(id, { model, state });
}

/** revealRangeInCenter 将 Range 滚动到中间，特别是长文件搜索时，需要一眼看到搜索内容 */
function revealRangeInCenter(range: IRange) {
  const instance = getEditorInstance();
  if (!instance) return;
  instance.revealRangeInCenter(range, editor.ScrollType.Smooth);
}

/** setSelection 将搜索结果默认被选中 */
async function setSelection(range: IRange) {
  await nextTick();
  const editorInstance = getEditorInstance();
  if (!editorInstance) return;
  editorInstance.focus();
  editorInstance.setSelection(range);
}

/** 获取编辑器的值 */
function getEditorContent() {
  return getEditorInstance()?.getValue();
}

/** 创建新的文本模型 */
function createModel(value: string, language: string) {
  return editor.createModel(value, language);
}

/** 设置新模型 */
function setModel(model: editor.ITextModel) {
  getEditorInstance()?.setModel(model);
}

/** 保存当前视图状态 */
function saveViewState() {
  return getEditorInstance()?.saveViewState();
}

/** 恢复视图状态 */
function restoreViewState(state: editor.ICodeEditorViewState | null) {
  getEditorInstance()?.restoreViewState(state);
}

/** 删除 viewState */
const removeViewState = (id: string) => viewStateMap.delete(id);

/**
 * 通过传入的后缀 获取Monaco language
 * @param s 后缀 suffix
 * @returns language
 */
function getLanguageModel(s: string) {
  const languageList = languages.getLanguages();
  const language = languageList.find((i) => i.extensions?.includes(`.${s}`));
  return language?.id || "javascript";
}

/**
 * 快捷键
 */
function keydownHandle(e: IEditorEvent) {
  // 通过keycode/ctrlKey/shiftKey/altKey 的状态唯一确定一个事件- 有值为true，无值为false
  const eventMap: IKeyMap<string, voidFun> = {
    "49/true/false/false": saveFileContent, // Ctrl + S
  };

  const key = `${e.keyCode}/${e.ctrlKey}/${e.shiftKey}/${e.altKey}`;

  if (!eventMap[key]) return;
  eventMap[key]();
  e.browserEvent.preventDefault();
}

/**
 * 保存文件内容
 */
async function saveFileContent() {
  // 1. 获取当前编辑器的内容-因为支持多tab 不能直接取当前，应该取当前的id对应的内容
  const contents = getEditorContent() as string;

  // 2. 调用 container 的 saveFile 方法
  await WebContainerAPI.writeFile(currentFileID.value, contents);

  ElMessage.success("保存成功");
}

onMounted(initEditor);

// 获取编辑器实例
const getEditorInstance = () => toRaw(editorInstance.value);

defineExpose({
  resize,
  destroy,
  initEditor,
  setEditorContent,
  getEditorContent,
  removeViewState,
  revealRangeInCenter,
  setSelection,
});
</script>

<style lang="css" scoped>
.editor-instance {
  height: 100%;
  flex: auto;
}
</style>
